#
# CS 143
#
# This file provides the Router class definition.
#
# @author Joe Wang
#

import copy, random, sys, string, time
import Routing, Queue
from Packet import Packet
from SimPy.Simulation import *

class Router(Process):
    '''The Router class represents a routing device on the network.

    '''

    # Metrics metric
    # What we log to, if it isn't None. Should NEVER BE NONE when running
    metrics = None
    
    def __init__(self, updateInterval, nics = []):
        '''Router class constructor.

        '''

        # Initialize parent.
        Process.__init__(self)

        # Set the router update period.
        self.__updateInterval = updateInterval

        # Set the router nics.
        self.__nics = nics
        
        # Set the router nic delays to be keyed by nic (value float)
        self.__nicDelays = {} # [0] * len(self.__nics)
        for nic in nics:
            self.__nicDelays[nic] = 0
        
        # RoutingTable __routingTable
        # Routing Table
        self.__routingTable = Routing.RoutingTable()

        # Set the initial routing table for all routers one hop away.
        for nic in nics:
            self.__addNIC(self.__routingTable, nic)
            
        self.__updateProcessingDelays()
        self.__lastUpdateTime = 0

    def addNIC(self, nic):
        self.__nics.append(nic)
        self.__addNIC(self.__routingTable, nic)
        
        self.__nicDelays[nic] = 0
        self.__updateProcessingDelays()
        self.__lastUpdateTime = 0

    def __addNIC(self, routingTable, nic):

        # Update the local routing table.
        routingTable[(nic.getIP(), nic.getSubnet())] = (nic, 0, None, 0)

    def go(self):
        '''Router class Process Execution Method.

        '''

        # The Router only provides the mechanics for determining which NIC to
        # route any given incoming packet. We assume the actual routing is
        # instantaneous, and that processing delays are simulated by the NICs.
        # Therefore, the "process execution" only need be to occasionally
        # updating the routing table.
        while True:

            # Only do this once per set of updates
            self.__updateProcessingDelays()
            self.__lastUpdateTime = now()
            # We periodically send out our routing information.
            self.__sendRouting()

            # Wait for the next update interval.
            yield hold, self, self.__updateInterval


    def send(self, packet):
        '''Sends the given packet.

        '''

        # Instrumentation
        self.metrics.log(self, now(), packet, 'RouterSending')

        # Determine the nic to send out the packet on, depending on the
        # destination.
        lookup = self.__routingTable[packet.destination]
        if lookup:
            (nic, _, nextHop, _) = lookup
            nic.setGateway(nextHop)
            nic.send(packet)

    def receive(self, nic, packet):
        '''Receive a packet from the given NIC. 

        '''
        
        # Determine where the packet is destined for:
        if packet.destination == (255, 255, 255, 255):
            self.__receiveRouting(nic, packet)

        else:
            # The packet is for forwarding.
            self.send(packet)

    def disconnect(self, nic):
        '''Handle the disconnection (not removal) of a nic.

        '''

        # Clear the routing table 
        del self.__routingTable[nic]

    def __updateProcessingDelays(self):
        '''Periodically update the cached processing and prop delays for your 
        NICs

        '''

        for i in range(len(self.__nicDelays)):

            # Iterate through each NIC
            nic = self.__nics[i]

            # Calculate new costs (static = link propagation delay; 
            #                      dynamic = average processing delay)
            self.__nicDelays[nic] = nic.getPropDelay() + \
                                    nic.getQueueingDelay()
        
    def __sendRouting(self):
        '''Send local routing table to neighboring routers.

        '''
        for i in range(len(self.__nics)):
            nic = self.__nics[i]
            packet = Packet()
            packet.source = nic.getIP()
            packet.destination = (255, 255, 255, 255)
            packet.size = 1000

            # Copy the object to prevent aliasing.
            packet.message = copy.copy(self.__routingTable)
            
            # Poison reverse for all routes that go through nic to prevent the
            # count to infinity problem.
            del packet.message[nic]

            # We exclude removing the subnet associated with the nic, however.
            self.__addNIC(packet.message, nic)

            for ((ip, subnet), cost) in packet.message:

                # In sending the routing table, we don't care about the local
                # nic or the local next hop, so we have those as empty
                # data slots. We fill one of them with the time the routing
                # table here was updated, in case we need to force an update.
                packet.message[(ip, subnet)] = (None, cost, None, self.__lastUpdateTime)

            # We don't use the router.send method, since each broadcast packet
            # source must correspond with the correct NIC.
            nic.send(packet)


    def __receiveRouting(self, nic, packet):
        '''Handle an incoming routing table.

        '''

        localChange = False
        
        # Update the local routing table.
        for ((ip, subnet), cost) in packet.message:
        
            # Add the delays associated with prop and queueing delay.
            cost += self.__nicDelays[nic]
        
            # Get the last updated time of the owner of this table.
            (_, _, _, newUpdateTime) = packet.message[ip]

            # If the specified IP/Subnet already exists in the routing table.
            if (ip, subnet) in self.__routingTable:

                # Get the current stats.
                (_, currentCost, currentNH, currentUpdateTime) = self.__routingTable[ip]
                
                # Let's not accidentally update our routing table for subnets
                # that we own.
                if currentNH:

                    # Switch our route if the cost is lower than the current
                    # or we need to update costs associated with the current
                    # route (increases, specifically).
                    if (currentCost > cost) or (newUpdateTime > currentUpdateTime and \
                            currentNH == packet.source):
                        print "updating tables", currentNH, currentCost, cost, newUpdateTime
                        self.__routingTable[(ip, subnet)] = (nic, cost, packet.source, newUpdateTime)
                        localChange = True

            # If the specified IP/Subnet doesn't already exist.
            else:
                self.__routingTable[(ip, subnet)] = (nic, cost, packet.source, newUpdateTime)
                localChange = True
        
        # Now that we've updated our routing table, if there were changes, we
        # broadcast our new table out to everyone.
        if localChange:
            self.__sendRouting()
